Step 2: CMake Language Fundamentals =================================== In the previous step we rushed through and handwaved several aspects of the CMake language which is used within ``CMakeLists.txt`` in order to get useful, building programs as soon as possible. However, in the wild we encounter a great deal more complexity than simply describing lists of source and header files. To deal with this complexity CMake provides a Turing-complete domain-specific language for describing the process of building software. Understanding the fundamentals of this language will be necessary as we write more complex CMLs and other CMake files. The language is formally known as ":manual:`CMake Language `", or more colloquially as CMakeLang. .. note:: The CMake Language is not well suited to describing things which are not related to building software. While it has some features for general purpose use, developers should use caution when solving problems not directly related to their build in CMake Language. Oftentimes the correct answer is to write a tool in a general purpose programming language which solves the problem, and teach CMake how to invoke that tool as part of the build process. Code generation, cryptographic signature utilities, and even ray-tracers have been written in CMake Language, but this is not a recommended practice. Because we want to fully explore the language features, this step is an exception to the tutorial sequencing. It neither builds on ``Step1``, nor is the starting point for ``Step3``. This will be a sandbox to explore language features without building any software. We'll pick back up with the Tutorial program in ``Step3``. .. note:: This tutorial endeavors to demonstrate best practices and solutions to real problems. However, for this one step we're going to be re-implementing some built-in CMake functions. In "real life", do not write your own :command:`list(APPEND)`. Background ^^^^^^^^^^ The only fundamental types in CMakeLang are strings and lists. Every object in CMake is a string, and lists are themselves strings which contain semicolons as separators. Any command which appears to operate on something other than a string, whether they be booleans, numbers, JSON objects, or otherwise, is in fact consuming a string, doing some internal conversion logic (in a language other than CMakeLang), and then converting back to a string for any potential output. We can create a variable, which is to say a name for a string, using the :command:`set` command. .. code-block:: cmake set(var "World!") A variable's value can be accessed using brace expansion, for example if we want to use the :command:`message` command to print the string named by ``var``. .. code-block:: cmake set(var "World!") message("Hello ${var}") .. code-block:: console $ cmake -P CMakeLists.txt Hello World! .. note:: :option:`cmake -P` is called "script mode", it informs CMake this file is not intended to have a :command:`project` command. We're not building any software, instead using CMake only as a command interpreter. Because CMakeLang has only strings, conditionals are entirely by convention of which strings are considered true and which are considered false. These are *supposed* to be intuitive, "True", "On", "Yes", and (strings representing) non-zero numbers are truthy, while "False" "Off", "No", "0", "Ignore", "NotFound", and the empty string are all considered false. However, some of the rules are more complex than that, so taking some time to consult the :command:`if` documentation on expressions is worthwhile. It's recommended to stick to a single pair for a given context, such as "True"/"False" or "On"/"Off". As mentioned, lists are strings containing semicolons. The :command:`list` command is useful for manipulating these, and many structures within CMake expect to operate with this convention. As an example, we can use the :command:`foreach` command to iterate over a list. .. code-block:: cmake set(stooges "Moe;Larry") list(APPEND stooges "Curly") message("Stooges contains: ${stooges}") foreach(stooge IN LISTS stooges) message("Hello, ${stooge}") endforeach() .. code-block:: console $ cmake -P CMakeLists.txt Stooges contains: Moe;Larry;Curly Hello, Moe Hello, Larry Hello, Curly Exercise 1 - Macros, Functions, and Lists ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CMake allows us to craft our own functions and macros. This can be very helpful when constructing lots of similar targets, like tests, for which we will want to call similar sets of commands over and over again. We do so with :command:`function` and :command:`macro`. .. code-block:: cmake macro(MyMacro MacroArgument) message("${MacroArgument}\n\t\tFrom Macro") endmacro() function(MyFunc FuncArgument) MyMacro("${FuncArgument}\n\tFrom Function") endfunction() MyFunc("From TopLevel") .. code-block:: console $ cmake -P CMakeLists.txt From TopLevel From Function From Macro Like with many languages, the difference between functions and macros is one of scope. In CMakeLang, both :command:`function` and :command:`macro` can "see" all the variables created in all the frames above them. However, a :command:`macro` acts semantically like a text replacement, similar to C/C++ macros, so any side effects the macro creates are visible in their calling context. If we create or change a variable in a macro, the caller will see the change. :command:`function` creates its own variable scope, so side effects are not visible to the caller. In order to propagate changes to the parent which called the function, we must use ``set( PARENT_SCOPE)``, which works the same as :command:`set` but for variables belonging to the caller's context. .. note:: In CMake 3.25, the :command:`return(PROPAGATE)` option was added, which works the same as :command:`set(PARENT_SCOPE)` but provides slightly better ergonomics. While not necessary for this exercise, it bears mentioning that :command:`macro` and :command:`function` both support variadic arguments via the ``ARGV`` variable, a list containing all arguments passed to the command, and the ``ARGN`` variable, containing all arguments past the last expected argument. We're not going to build any targets in this exercise, so instead we'll construct our own version of :command:`list(APPEND)`, which adds a value to a list. Goal ---- Implement a macro and a function which append a value to a list, without using the :command:`list(APPEND)` command. The desired usage of these commands is as follows: .. code-block:: cmake set(Letters "Alpha;Beta") MacroAppend(Letters "Gamma") message("Letters contains: ${Letters}") .. code-block:: console $ cmake -P Exercise1.cmake Letters contains: Alpha;Beta;Gamma .. note:: The extension for these exercises is ``.cmake``, that's the standard extension for CMakeLang files when not contained in a ``CMakeLists.txt`` Helpful Resources ----------------- * :command:`macro` * :command:`function` * :command:`set` * :command:`if` Files to Edit ------------- * ``Exercise1.cmake`` Getting Started ---------------- The source code for ``Exercise1.cmake`` is provided in the ``Help/guide/tutorial/Step2`` directory. It contains tests to verify the append behavior described above. .. note:: You're not expected to handle the case of an empty or undefined list to append to. However, as a bonus, the case is tested if you want to try out your understanding of CMakeLang conditionals. Complete ``TODO 1`` and ``TODO 2``. Build and Run ------------- We're going to use script mode to run these exercises. First navigate to the ``Help/guide/tutorial/Step2`` folder then you can run the code with: .. code-block:: console cmake -P Exercise1.cmake The script will report if the commands were implemented correctly. Solution -------- This problem relies on an understanding of the mechanisms of CMake variables. CMake variables are names for strings; or put another way, a CMake variable is itself a string which can brace expand into a different string. This leads to a common pattern in CMake code where functions and macros aren't passed values, but rather, they are passed the names of variables which contain those values. Thus ``ListVar`` does not contain the *value* of the list we need to append to, it contains the *name* of a list, which contains the value we need to append to. When expanding the variable with ``${ListVar}``, we will get the name of the list. If we expand that name with ``${${ListVar}}``, we will get the values the list contains. To implement ``MacroAppend``, we need only combine this understanding of ``ListVar`` with our knowledge of the :command:`set` command. .. raw:: html
TODO 1: Click to show/hide answer .. code-block:: cmake :caption: TODO 1: Exercise1.cmake :name: Exercise1.cmake-MacroAppend macro(MacroAppend ListVar Value) set(${ListVar} "${${ListVar}};${Value}") endmacro() .. raw:: html
We don't need to worry about scope here, because a macro operates in the same scope as its parent. ``FuncAppend`` is almost identical, in fact it could be implemented in the same one liner but with an added ``PARENT_SCOPE``, but the instructions ask us to implement it in terms of ``MacroAppend``. .. raw:: html
TODO 2: Click to show/hide answer .. code-block:: cmake :caption: TODO 2: Exercise1.cmake :name: Exercise1.cmake-FuncAppend function(FuncAppend ListVar Value) MacroAppend(${ListVar} ${Value}) set(${ListVar} "${${ListVar}}" PARENT_SCOPE) endfunction() .. raw:: html
``MacroAppend`` transforms ``ListVar`` for us, but it won't propagate the result to the parent scope. Because this is a function, we need to do so ourselves with :command:`set(PARENT_SCOPE)`. Exercise 2 - Conditionals and Loops ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The two most common flow control elements in any structured programming language are conditionals and their close sibling loops. CMakeLang is no different. As previously mentioned, the truthiness of a given CMake string is a convention established by the :command:`if` command. When given a string, :command:`if` will first check if it is one of the known constant values previously discussed. If the string isn't one of those values the command assumes it is a variable, and checks the brace-expanded contents of that variable to determine the result of the conditional. .. code-block:: cmake if(True) message("Constant Value: True") else() message("Constant Value: False") endif() if(ConditionalValue) message("Undefined Variable: True") else() message("Undefined Variable: False") endif() set(ConditionalValue True) if(ConditionalValue) message("Defined Variable: True") else() message("Defined Variable: False") endif() .. code-block:: console $ cmake -P ConditionalValue.cmake Constant Value: True Undefined Variable: False Defined Variable: True .. note:: This is a good a time as any to discuss quoting in CMake. All objects in CMake are strings, thus the double quote, ``"``, is often unnecessary. CMake knows the object is a string, everything is a string. However, it is needed in some contexts. Strings containing whitespace require double quotes, else they are treated like lists; CMake will concatenate the elements together with semicolons. The reverse is also true, when brace-expanding lists it is necessary to do so inside quotes if we want to *preserve* the semicolons. Otherwise CMake will expand the list items into space-separate strings. A handful of commands, such as :command:`if`, recognize the difference between quoted and unquoted strings. :command:`if` will only check that the given string represents a variable when the string is unquoted. Finally, :command:`if` provides several useful comparison modes such as ``STREQUAL`` for string matching, ``DEFINED`` for checking the existence of a variable, and ``MATCHES`` for regular expression checks. It also supports the typical logical operators, ``NOT``, ``AND``, and ``OR``. In addition to conditionals CMake provides two loop structures, :command:`while`, which follows the same rules as :command:`if` for checking a loop variable, and the more useful :command:`foreach`, which iterates over lists of strings and was demonstrated in the `Background`_ section. For this exercise, we're going to use loops and conditionals to solve some simple problems. We'll be using the aforementioned ``ARGN`` variable from :command:`function` as the list to operate on. Goal ---- Loop over a list, and return all the strings containing the string ``Foo``. .. note:: Those who read the command documentation will be aware that this is :command:`list(FILTER)`, resist the temptation to use it. Helpful Resources ----------------- * :command:`function` * :command:`foreach` * :command:`if` * :command:`list` Files to Edit ------------- * ``Exercise2.cmake`` Getting Started ---------------- The source code for ``Exercise2.cmake`` is provided in the ``Help/guide/tutorial/Step2`` directory. It contains tests to verify the append behavior described above. .. note:: You should use the :command:`list(APPEND)` command this time to collect your final result into a list. The input can be consumed from the ``ARGN`` variable of the provided function. Complete ``TODO 3``. Build and Run ------------- Navigate to the ``Help/guide/tutorial/Step2`` folder then you can run the code with: .. code-block:: console cmake -P Exercise2.cmake The script will report if the ``FilterFoo`` function was implemented correctly. Solution -------- We need to do three things, loop over the ``ARGN`` list, check if a given item in that list matches ``"Foo"``, and if so append it to the ``OutVar`` list. While there are a couple ways we could invoke :command:`foreach`, the recommended way is to allow the command to do the variable expansion for us via ``IN LISTS`` to access the ``ARGN`` list items. The :command:`if` comparison we need is ``MATCHES`` which will check if ``"FOO"`` exists in the item. All that remains is to append the item to the ``OutVar`` list. The trickiest part is remembering that ``OutVar`` *names* a list, it is not the list itself, so we need to access it via ``${OutVar}``. .. raw:: html
TODO 3: Click to show/hide answer .. code-block:: cmake :caption: TODO 3: Exercise2.cmake :name: Exercise2.cmake-FilterFoo function(FilterFoo OutVar) foreach(item IN LISTS ARGN) if(item MATCHES Foo) list(APPEND ${OutVar} ${item}) endif() endforeach() set(${OutVar} ${${OutVar}} PARENT_SCOPE) endfunction() .. raw:: html
Exercise 3 - Organizing with Include ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We have already discussed how to incorporate subdirectories containing their own CMLs with :command:`add_subdirectory`. In later steps we will explore the various way CMake code can be packaged and shared across projects. However for small CMake functions and utilities, it is often beneficial for them to live in their own ``.cmake`` files outside the project CMLs and separate from the rest of the build system. This allows for separation of concerns, removing the project-specific elements from the utilities we are using to describe them. To incorporate these separate ``.cmake`` files into our project, we use the :command:`include` command. This command immediately begins interpreting the contents of the :command:`include`'d file in the scope of the parent CML. It is as if the entire file were being called as a macro. Traditionally, these kinds of ``.cmake`` files live in a folder named "cmake" inside the project root. For this exercise, we'll use the ``Step2`` folder instead. Goal ---- Use the functions from Exercises 1 and 2 to build and filter our own list of items. Helpful Resources ----------------- * :command:`include` Files to Edit ------------- * ``Exercise3.cmake`` Getting Started ---------------- The source code for ``Exercise3.cmake`` is provided in the ``Help/guide/tutorial/Step2`` directory. It contains tests to verify the correct usage of our functions from the previous two exercises. .. note:: Actually it reuses tests from Exercise2.cmake, reusable code is good for everyone. Complete ``TODO 4`` through ``TODO 7``. Build and Run ------------- Navigate to the ``Help/guide/tutorial/Step2`` folder then you can run the code with: .. code-block:: console cmake -P Exercise3.cmake The script will report if the functions were invoked and composed correctly. Solution -------- The :command:`include` command will interpret the included file completely, including the tests from the first two exercises. We don't want to run these tests again. Thanks to some forethought, these files check a variable called ``SKIP_TESTS`` prior to running their tests, setting this to ``True`` will get us the behavior we want. .. raw:: html
TODO 4: Click to show/hide answer .. code-block:: cmake :caption: TODO 4: Exercise3.cmake :name: Exercise3.cmake-SKIP_TESTS set(SKIP_TESTS True) .. raw:: html
Now we're ready to :command:`include` the previous exercises to grab their functions. .. raw:: html
TODO 5: Click to show/hide answer .. code-block:: cmake :caption: TODO 5: Exercise3.cmake :name: Exercise3.cmake-include include(Exercise1.cmake) include(Exercise2.cmake) .. raw:: html
Now that ``FuncAppend`` is available to us, we can use it to append new elements to the ``InList``. .. raw:: html
TODO 6: Click to show/hide answer .. code-block:: cmake :caption: TODO 6: Exercise3.cmake :name: Exercise3.cmake-FuncAppend FuncAppend(InList FooBaz) FuncAppend(InList QuxBaz) .. raw:: html
Finally, we can use ``FilterFoo`` to filter the full list. The tricky part to remember here is that our ``FilterFoo`` wants to operate on list values via ``ARGN``, so we need to expand the ``InList`` when we call ``FilterFoo``. .. raw:: html
TODO 7: Click to show/hide answer .. code-block:: cmake :caption: TODO 7: Exercise3.cmake :name: Exercise3.cmake-FilterFoo FilterFoo(OutList ${InList}) .. raw:: html